import type { Response, NextFunction } from 'express'

import type { ExtendedRequest, Context, Tree, ToC } from '@/types'
import findPageInSiteTree from '@/frame/lib/find-page-in-site-tree'

function isNewLandingPage(currentLayoutName: string): boolean {
  return (
    currentLayoutName === 'category-landing' ||
    currentLayoutName === 'bespoke-landing' ||
    currentLayoutName === 'discovery-landing' ||
    currentLayoutName === 'journey-landing'
  )
}

// This module adds either flatTocItems or nestedTocItems to the context object for
// product, category, and subcategory TOCs that don't have other layouts specified.
// They are rendered by includes/generic-toc-flat.html or includes/generic-toc-nested.html.
export default async function genericToc(req: ExtendedRequest, res: Response, next: NextFunction) {
  if (!req.context) throw new Error('request not contextualized')
  if (!req.context.page) return next()
  if (
    req.context.currentLayoutName !== 'default' &&
    !isNewLandingPage(req.context.currentLayoutName || '')
  )
    return next()
  // This middleware can only run on product, category, and subcategories.
  if (
    req.context.page.documentType === 'homepage' ||
    req.context.page.documentType === 'article' ||
    req.context.page.relativePath === 'search/index.md'
  )
    return next()

  // This one product TOC is weird.
  const isOneOffProductToc = req.context.page.relativePath === 'github/index.md'

  // There are different types of TOC depending on the document type.
  const tocTypes: Record<string, string> = {
    product: 'flat',
    category: 'nested',
    subcategory: 'flat',
  }

  // Frontmatter can optionally be set on an Early Access product to show hidden child items.
  // If so, this is a special case where we want to override the flat tocType and use a nested type.
  const earlyAccessToc = req.context.page.earlyAccessToc

  if (!req.context.currentProductTree) throw new Error('currentProductTree not in context')
  if (!req.context.currentEnglishTree) throw new Error('currentEnglishTree not in context')
  if (!req.pagePath) throw new Error('pagePath not in request')

  // Find the part of the site tree that corresponds to the current path.
  const treePage = findPageInSiteTree(
    req.context.currentProductTree,
    req.context.currentEnglishTree,
    req.pagePath,
  )

  let fauxSubcategory = false
  if (req.context.page.documentType === 'category' && req.context.page.autogenerated !== 'rest') {
    // But does *have* children?
    const hasGrandchildren = (treePage.childPages || []).some((child) => child.children)
    fauxSubcategory = !hasGrandchildren
  }

  // Find the current TOC type based on the current document type.
  const currentTocType = earlyAccessToc
    ? 'nested'
    : fauxSubcategory
      ? 'flat'
      : tocTypes[req.context.page.documentType]

  // By default, only include hidden child items on a TOC page if it's an Early Access category or
  // subcategory page, not a product or 'articles' fake category page (e.g., /early-access/github/articles).
  // This is because we don't want entire EA product TOCs to be publicly browseable, but anything at the category
  // or below level is fair game because that content is scoped to specific features.
  const isCategoryOrSubcategory =
    req.context.page.documentType === 'category' || req.context.page.documentType === 'subcategory'
  if (!req.context.currentPath) throw new Error('currentPath not in context')
  const isEarlyAccess = req.context.currentPath.includes('/early-access/')
  const isArticlesCategory = req.context.currentPath.endsWith('/articles')

  const includeHidden =
    earlyAccessToc || (isCategoryOrSubcategory && isEarlyAccess && !isArticlesCategory)

  // Conditionally run getTocItems() recursively.
  let isRecursive
  // Conditionally render intros.
  let renderIntros

  // Get an array of child links with intros and add it to the context object.
  if (currentTocType === 'flat' && !isOneOffProductToc) {
    isRecursive = false
    renderIntros = true
    req.context.genericTocFlat = await getTocItems(treePage, req.context, {
      recurse: isRecursive,
      renderIntros,
      includeHidden,
      textOnly: isNewLandingPage(req.context.currentLayoutName || ''),
    })
  }

  // Get an array of child subcategories and their child articles and add it to the context object.
  if (currentTocType === 'nested' || isOneOffProductToc) {
    isRecursive = !isOneOffProductToc
    renderIntros = false
    req.context.genericTocNested = await getTocItems(treePage, req.context, {
      recurse: isRecursive,
      renderIntros: isNewLandingPage(req.context.currentLayoutName || '') ? true : false,
      includeHidden,
      textOnly: isNewLandingPage(req.context.currentLayoutName || ''),
    })
  }

  return next()
}

// Return a nested object that contains the bits and pieces we need
// for the tree which is used for sidebars and listing
type Options = {
  recurse: boolean
  renderIntros: boolean
  includeHidden: boolean
  textOnly: boolean
}

async function getTocItems(node: Tree, context: Context, opts: Options): Promise<ToC[]> {
  // Cleaner than trying to be too terse inside the `.filter()` inline callback.
  function filterHidden(child: Tree): boolean {
    return opts.includeHidden || !child.page.hidden
  }

  return await Promise.all(
    node.childPages.filter(filterHidden).map(async (child) => {
      const { page } = child
      const title = await page.renderProp('rawTitle', context, { textOnly: true })
      const octicon = page.octicon ?? null
      const category = page.category ? page.category : null
      const complexity = page.complexity ? page.complexity : null
      const industry = page.industry ? page.industry : null
      let intro = null
      if (opts.renderIntros) {
        intro = ''
        if (page.rawIntro) {
          // The intro can contain Markdown even though it might not
          // contain any Liquid.
          // Use textOnly for new landing pages to strip HTML tags.
          // For other pages, we intend to display the intro in a table of contents
          // component with the HTML (dangerouslySetInnerHTML).
          intro = await page.renderProp(
            'rawIntro',
            context,
            opts.textOnly ? { textOnly: true } : {},
          )
        }
      }

      const childTocItems = []
      if (child.childPages) {
        childTocItems.push(...(await getTocItems(child, context, opts)))
      }

      const fullPath = child.href
      return {
        title,
        fullPath,
        intro,
        octicon,
        category,
        complexity,
        industry,
        childTocItems,
      } as ToC
    }),
  )
}
